﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using FMOD;

using Rockfan.Framework;
using Rockfan.Framework.Audio;
using Rockfan.Framework.Entities;
using Rockfan.Framework.Entities.Interfaces;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;


namespace Rockfan.Framework.Audio
{
    public sealed class AudioTrack : ISound
    {
        public event AudioEventHandler TrackCompleted;

        #region Properties

        /// <summary>
        /// Gets a string value representing the name of this object
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// Gets a string value representing the location on disk to load the sound data
        /// </summary>
        public string FileLocation { get; private set; }

        /// <summary>
        /// Gets a value indicating an amount in seconds when this object was created
        /// </summary>
        public float TimeCreated { get; private set; }

        /// <summary>
        /// Gets a value indicating the total length in seconds of this object
        /// </summary>
        public float Playlength
        {
            get { return _Playlength; }
        }

        /// <summary>
        /// Gets or Sets an amount silence before the Track begins while in playback,
        /// This property will only take effect, if the Track is at playposition 0
        /// </summary>
        public float Delay
        {
            get { return _Delay; }
            set { SetDelay(value); }            
        }

        /// <summary>
        /// Gets a value indicating whether this object is currently playing
        /// </summary>
        public bool IsPlaying
        {
            get
            {
                if (Channel == null)
                    return false;

                var isPaused = false;
                Channel.getPaused(ref isPaused);

                return !isPaused && _IsPlaying;
            }
        }

        /// <summary>
        /// Gets or Sets a value indicating the volume at which this object is played
        /// </summary>
        public float Volume
        {
            get { return _Volume; }
            set
            {
                var oldValue = Volume;
                _Volume = MathHelper.Clamp(value, 0.0f, 100.0f);

                if (Volume != oldValue)
                    SetVolume(Volume);
            }
        }

        /// <summary>
        /// Gets or Sets a value indicating a percetage of the base playspeed, that this object should be played
        /// </summary>
        public float Playspeed
        {
            get { return _Playspeed; }
            set
            {
                var oldValue = Playspeed;
                _Playspeed = value;

                if (Playspeed >= 0.0f && Playspeed <= 50.0f)
                    _Playspeed = MathHelper.Clamp(_Playspeed, 25.0f, 400.0f);

                // Support Reverse Playback
                if (Playspeed < 0.0f && Playspeed >= -10.0f)
                    _Playspeed = MathHelper.Clamp(_Playspeed, -400.0f, -10.0f);

                if (Playspeed != oldValue)
                    SetPlayspeed(Playspeed);
            }
        }

        /// <summary>
        /// Gets or Sets the position within the playLength in seconds of this object
        /// </summary>
        public float Playposition
        {
            get { return _Playposition; }
            set
            {
                var oldValue = Playposition;
                _Playposition = MathHelper.Clamp(value, 0.0f, Playlength);

                if (Playposition != oldValue)
                    SetPlayposition(Playposition);
            }
        }

        /// <summary>
        /// Gets or Sets the panning on the left and right channels
        /// </summary>
        public float Pan
        {
            get { return _Panning; }
            set
            {
                var oldValue = Pan;
                _Panning = MathHelper.Clamp(value, -1.0f, 1.0f);

                if (Pan != oldValue)
                    SetPan(Pan);
            }
        }

        /// <summary>
        /// Gets or Sets value indicating the desired pitch of this object
        /// </summary>
        public float Pitch
        {
            get { return _Pitch; }
            set
            {
                var oldValue = Pitch;
                _Pitch = MathHelper.Clamp(value, 0.5f, 2.0f);

                if (Pitch != oldValue)
                    SetPitch(Pitch);
            }
        }

        // We are not looping AudioTracks
        bool ISound.IsLooped { get { return false; } }

        // We ALWAYS need to be updated, to remove channels, so the system can dispose of them
        bool IUpdateableEntity.EnableUpdates { get { return true; } set { } }

        // Add Support for various DSP Effects

        #endregion

        #region Methods

        #region Constructor

        /// <summary>
        /// Constructs a new instance of this object
        /// </summary>
        /// <param name="fileLocation">a string value reprsenting the location on disk for this data</param>
        internal AudioTrack(string fileLocation)
        {
            Timer = new Stopwatch();

            if (File.Exists(fileLocation))
            {
                Sound = AudioEngine.CreateSound(fileLocation, false);
                LastError = AudioEngine.GetLastError();

                if (LastError == RESULT.OK)
                {
                    var playLength = (uint)0;
                    Sound.getLength(ref playLength, TIMEUNIT.MS);
                    LastError = AudioEngine.GetLastError();

                    _Playlength = (float)(playLength / 1000.0f);
                    Pitchshift = AudioEngine.CreateDSP(DSP_TYPE.PITCHSHIFT);
                    LastError = AudioEngine.GetLastError();

                    if (LastError == RESULT.OK)
                        LastError = Pitchshift.setParameter((int)DSP_PITCHSHIFT.PITCH, 1.0f);
                }

                _Volume = 100.0f;
                _Playspeed = 100.0f;
                _Playposition = 0.0f;
                _Panning = 0.0f;
                _Pitch = 1.0f;
                _Delay = 0.0f;

                Name = Path.GetFileNameWithoutExtension(fileLocation);
                TimeCreated = EngineServices.TotalGameTime;                
            }
            else LastError = RESULT.ERR_FILE_NOTFOUND;

            AudioEngine.AddSound(this);
        }

        #endregion

        public void Play()
        {
            if (Channel == null)
            {
                CheckLastError();

                Channel = default(Channel);
                AudioEngine.PlaySound(Sound, ref Channel);

                if (AudioEngine.GetLastError() != RESULT.OK)
                    throw new AudioException(Error.String(AudioEngine.GetLastError()));

                if (BaseFrequency == 0 || BaseVolume == 0)
                {
                    Channel.getFrequency(ref BaseFrequency);
                    Channel.getVolume(ref BaseVolume);
                    Channel.addDSP(Pitchshift, ref DSPConnection);
                }

                SetPan(Pan);
                SetPitch(Pitch);
                SetVolume(Volume);
                SetPlayposition(Playposition);
                SetPlayspeed(Playspeed);

                if (Delay == 0)
                    Channel.setPaused(false);

                else Timer.Start();
            }
            else if (Channel != null && !IsPlaying)
                Channel.setPaused(false);

            _IsPlaying = true;
        }

        public void Pause()
        {
            if (Channel != null)
                Channel.setPaused(true);            
        }

        public void Stop()
        {
            if (Channel != null)
            {
                Playposition = 0.0f;
                Channel.setPaused(true);

                _IsPlaying = false;

                if (TrackCompleted != null)
                    TrackCompleted(this, AudioEventType.Completed);
            }
        }

        // Check the last Error from the AudioEngine
        private void CheckLastError()
        {
            if (LastError != RESULT.OK)
                throw new AudioException(Error.String(LastError));
        }

        private void SetPan(float value)
        {
            if (Channel != null)
                Channel.setPan(value);
        }

        private void SetDelay(float value)
        {
            if (Channel != null)
            {
                Timer.Stop();

                var playLength = (uint)0;
                var playPosition = (uint)0;
                var delay = Math.Max(0.0f, value);
                var currentTime = (float)(Timer.ElapsedTicks / (float)Stopwatch.Frequency);
                var delayOffset = delay - (_Delay - (currentTime - _OldDelayTime));
                
                Sound.getLength(ref playLength, TIMEUNIT.MS);               
                Channel.getPosition(ref playPosition, TIMEUNIT.MS);

                _Delay = delay;
                _Playlength = (playLength / 1000.0f) + delay;

                if (delayOffset >= (playPosition / 1000.0f))
                {
                    Channel.setPaused(true);
                    Channel.setPosition(0, TIMEUNIT.MS);
                    _OldDelayTime += delayOffset;
                }
                else Channel.setPosition(playPosition - (uint)(delay * 1000.0f), TIMEUNIT.MS);

                (this as IUpdateableEntity).Update(0.0f);
            }
            else _Delay = Math.Max(0.0f, value);
        }

        private void SetPitch(float value)
        {
            if (Channel != null && Pitchshift != null)
            {
                Pitchshift.setBypass(value == 1.0f);
                Pitchshift.setParameter((int)DSP_PITCHSHIFT.PITCH, value);
            }
        }

        private void SetVolume(float value)
        {
            if (Channel != null)
                Channel.setVolume(BaseVolume * (value / 100.0f));
        }

        private void SetPlayspeed(float value)
        {
            if (Channel != null)
                Channel.setFrequency(BaseFrequency * (value / 100.0f));
        }

        private void SetPlayposition(float value)
        {
            if (Channel != null)
            {
                var truePosition = Math.Max(0.0f, (value - Delay) * 1000.0f);
                Channel.setPosition((uint)((value - Delay) * 1000.0f), TIMEUNIT.MS);

                (this as IUpdateableEntity).Update(0.0f);
            }
        }

        void IUpdateableEntity.Update(float delta)
        {
            CheckLastError();

            if (Channel != null)
            {
                var isPaused = false;
                Channel.getPaused(ref isPaused);

                if (isPaused && _IsPlaying)
                {
                    Timer.Stop();
                    _DelayTime = (float)(Timer.ElapsedTicks / (float)Stopwatch.Frequency) - _OldDelayTime;

                    if (_DelayTime >= Delay)
                    {
                        var offset = _DelayTime - Delay;
                        Channel.setPosition((uint)(offset * 1000.0f), TIMEUNIT.MS);
                        Channel.setPaused(false);
                        _OldDelayTime = _DelayTime; // Set the oldDelay time, just incase someone decides to change the delay while playing
                    }
                    else Timer.Start();
                }
                else if (IsPlaying)
                {
                    var playPosition = (uint)0;
                    Channel.getPosition(ref playPosition, TIMEUNIT.MS);
                    _Playposition = (float)(playPosition / 1000.0f) + _DelayTime;
                }
            }
        }

        // Should only be destroyed by the Song in which created it
        void IDestroyable.Destroy()
        {
            Stop();

            if (Channel != null)
                Channel.stop();

            if (Sound != null)
                Sound.release();

            _Volume = 0.0f;
            _Playspeed = 0.0f;
            _Playlength = 0.0f;
            _Playposition = 0.0f;
            BaseVolume = 0;
            BaseFrequency = 0;
            Sound = null;
            LastError = RESULT.OK;
            Channel = null;

            AudioEngine.Remove(this);
            GC.SuppressFinalize(this);
        }

        #endregion

        private bool _IsPlaying;

        private float _OldDelayTime;
        private float _DelayTime;
        private float _Volume;
        private float _Playspeed;
        private float _Playlength;
        private float _Playposition;
        private float _Panning;
        private float _Pitch;
        private float _Delay;

        private float BaseVolume;
        private float BaseFrequency;
        
        private Sound Sound;        
        private Channel Channel;
        private RESULT LastError;

        private Stopwatch Timer;

        private DSP Pitchshift;
        private DSPConnection DSPConnection;
    }
}
